﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml.Serialization;

namespace Senthil.Mobile.WhereAmI
{
    [Serializable]
    public class Route
    {
        RoutePoint startingPoint;
        RoutePoint lastAddedPoint;

        Dictionary<Location, RoutePoint> locationRoutePointMap = new Dictionary<Location,RoutePoint>();
        List<LocationGroup> locationGroups = new List<LocationGroup>();
        Dictionary<string, List<Location>> locations = new Dictionary<string, List<Location>>();
        Dictionary<string, Location> cellTowerIdLocations = new Dictionary<string, Location>();
        Dictionary<Guid, Location> idLocationMap = new Dictionary<Guid, Location>();

        internal Route(string name)
            : this(name, ModeOfTravel.Rail)
        { }

        internal Route(string name, ModeOfTravel modeOfTravel)
        {
            this.Name = name;
            this.ModeOfTravel = modeOfTravel;
            this.OperatorName = "";
        }

        public Route() { }

        public string Name { get; set; }
        public ModeOfTravel ModeOfTravel { get; set; }
        public string OperatorName { get; set; }

        public LocationGroup[] LocationGroups
        {
            get { return locationGroups.ToArray(); }
            set
            {   
                foreach (LocationGroup group in value)
                {
                    AddLocationGroup(group);
                }
            }
        }

        RoutePoint[] deserializedRoutePoints;
        public RoutePoint[] RoutePoints
        {
            get { return locationRoutePointMap.Values.ToArray(); }
            set
            {
                deserializedRoutePoints = value;
            }
        }

        [XmlIgnore]
        public Location Start
        {
            get { return startingPoint.Location; }
        }

        [XmlIgnore]
        public Location Destination
        {
            get { return lastAddedPoint.Location; }
        }

        [XmlIgnore]
        public LocationGroup StartGroup
        {
            get { return Start.LocationGroup; }
        }

        [XmlIgnore]
        public LocationGroup DestinationGroup
        {
            get { return Destination.LocationGroup; }
        }

        public Location GetNextLocation(Location lastKnownLocation)
        {
            RoutePoint nextPoint = locationRoutePointMap[lastKnownLocation].Next;
            return nextPoint != null ? nextPoint.Location : null;
        }

        public LocationGroup GetNextMajorLocationGroup(Location lastKnownLocation)
        {
            RoutePoint routePoint = ForeachRoutePoint(lastKnownLocation,
                ((location) => location.LocationGroup != lastKnownLocation.LocationGroup && location.LocationGroup.IsSpecial));

            return routePoint != null ? routePoint.Location.LocationGroup : null;
        }

        public IEnumerable<LocationGroup> GetLocationGroupsAfter(Location lastKnownLocation)
        {
            LocationGroup currentLocationGroup = lastKnownLocation.LocationGroup;

            RoutePoint routePoint = null;

            if (!locationRoutePointMap.TryGetValue(lastKnownLocation, out routePoint))
                routePoint = startingPoint;

            while (routePoint != null && routePoint.Location.LocationGroup != currentLocationGroup)
                    routePoint = routePoint.Next;

            while (routePoint != null)
            {
                LocationGroup group = routePoint.Location.LocationGroup;

                if (group != currentLocationGroup)
                {
                    yield return group;
                }
                
                routePoint = routePoint.Next;
            }
        }

        private RoutePoint ForeachRoutePoint(Location start, Predicate<Location> predicate)
        {
            RoutePoint currentPoint = null;

            if (!locationRoutePointMap.TryGetValue(start, out currentPoint))
                currentPoint = startingPoint;

            while (currentPoint != null)
            {
                Location location = currentPoint.Location;

                if (predicate(location))
                {
                    return currentPoint;
                }

                currentPoint = currentPoint.Next;
            }

            return null;
        }

        public void AddNextRoutePoint(Location location, TimeSpan timeToTravelFromPreviousRoutePoint)
        {
            RoutePoint routePoint = new RoutePoint(location, timeToTravelFromPreviousRoutePoint);
            AddNextRoutePoint(routePoint);
        }

        internal void AddNextRoutePoint(RoutePoint routePoint)
        {
            if (startingPoint == null)
            {
                startingPoint = routePoint;
                startingPoint.RelativeIndexInRoute = 0;
            }

            if (lastAddedPoint != null)
            {
                this.lastAddedPoint.Next = routePoint;
                routePoint.Previous = this.lastAddedPoint;
                routePoint.RelativeIndexInRoute = this.lastAddedPoint.RelativeIndexInRoute + 1;
            }

            this.lastAddedPoint = routePoint;
            locationRoutePointMap[routePoint.Location] = this.lastAddedPoint;
        }

        public TimeSpan GetAverageTimeTaken(Location from, IEnumerable<Location> to)
        {
            RoutePoint fromPoint = locationRoutePointMap[from];

            IEnumerable<Location> toLocations = to.SkipWhile((location) => fromPoint.RelativeIndexInRoute >= locationRoutePointMap[location].RelativeIndexInRoute);

            if (!toLocations.Any())
            {
                return TimeSpan.Zero;
            }

            double milliseconds = toLocations.Average((location) =>
             GetTimeTaken(from, location).TotalMilliseconds);

            return TimeSpan.FromMilliseconds(milliseconds);
        }

        public TimeSpan GetTimeTaken(Location from, Location to)
        {
            if (from == to)
            {
                return TimeSpan.Zero;
            }

            RoutePoint fromPoint;

            if (!locationRoutePointMap.TryGetValue(from, out fromPoint))
            {
                throw new InvalidOperationException("Location " + from + " not found in Route " + this.Name);
            }

            RoutePoint nextPoint = null;
            TimeSpan timeSpan = TimeSpan.Zero;
            bool toLocationFound = false;

            while ((nextPoint = fromPoint.Next) != null)
            {
                timeSpan = timeSpan.Add(nextPoint.TimeFromPreviousRoutePoint);

                if (nextPoint.Location == to)
                {
                    toLocationFound = true;
                    break;
                }
                
                fromPoint = nextPoint;
            }

            if (!toLocationFound)
            {
                throw new InvalidOperationException("Location " + to + " not found in Route " + this.Name + " after location " + from);
            }

            return timeSpan;
        }

        public void AddLocationGroup(LocationGroup locationGroup)
        {
            locationGroups.Add(locationGroup);

            foreach (Location l in locationGroup.Locations)
            {
                InsertNewLocation(l);
            }
        }

        public void Disassociate(LocationGroup locationGroup)
        {
            locationGroups.Remove(locationGroup);
            locationGroup.RemoveAllLocations();
        }

        public void Associate(LocationGroup locationGroup)
        {
            locationGroups.Add(locationGroup);
        }
        

        // obsolete, need to remove
        public Location Associate(LocationGroup group, string loc)
        {
            List<Location> associatedLocations;

            Location location = null;
            if (!locations.TryGetValue(loc, out associatedLocations))
            {
                location = new Location(loc);
                InsertNewLocation(location);
            }
            else
            {
                location = associatedLocations[0];
            }

            group.AddLocation(location);

            return location;
        }

        private void InsertNewLocation(Location location)
        {
            idLocationMap[location.Id] = location;

            List<Location> associatedLocations;
            if (!locations.TryGetValue(location.Name, out associatedLocations))
            {
                associatedLocations = new List<Location>();
                locations[location.Name] = associatedLocations;
            }

            associatedLocations.Add(location);

            if (!string.IsNullOrEmpty(location.CellTowerId))
                cellTowerIdLocations.Add(location.CellTowerId, location);
        }

        public bool TryGetFirstLocationWithNameAndCellTowerId(string locationName, string cellTowerId, out Location location)
        {
            location = null;
           List<Location> result;
            
            if (TryGetLocation(locationName, cellTowerId, out result))
            {
                // Assumes that locations are added to the locations dict in order of appearance.
                location = result.First();
                return true;
            }

            return false;
        }

        public bool TryGetLocation(string locationName, string cellTowerId, out List<Location> location)
        {
            // Cell tower id is a stronger candidate than location name, so check it first
            if (!string.IsNullOrEmpty(cellTowerId))
            {
                Location loc;
                if (cellTowerIdLocations.TryGetValue(cellTowerId, out loc))
                {
                    location = new List<Location>() { loc };
                    return true;
                }
            }

            return locations.TryGetValue(locationName, out location);
        }

        public bool TryGetClosestLocation(string locationName, string cellTowerId, Location lastKnownLocation, out Location location)
        {
            List<Location> associatedLocations;
            location = null;

            if (!TryGetLocation(locationName, cellTowerId, out associatedLocations))
                return false;

            if (associatedLocations.Count == 1)
            {
                location = associatedLocations[0];
                return true;
            }

            List<Location> locationsWithSameLocationGroup = associatedLocations.FindAll((locationArg) => locationArg.LocationGroup == lastKnownLocation.LocationGroup);

            if (locationsWithSameLocationGroup.Count() == 1)
            {
                location = locationsWithSameLocationGroup[0];
                return true;
            }

            int minDistance = int.MaxValue;
            foreach (Location associatedLocation in associatedLocations)
            {
                RoutePoint routePoint = locationRoutePointMap[lastKnownLocation];
                RoutePoint associatedLocationRoutePoint = locationRoutePointMap[associatedLocation];

                int distance = associatedLocationRoutePoint.RelativeIndexInRoute - routePoint.RelativeIndexInRoute;

                if (distance < 0)
                {
                    continue;
                }

                if (distance < minDistance)
                {
                    location = associatedLocationRoutePoint.Location;
                    minDistance = distance;
                }
            }

            return true;
        }

        public void GetTimeOffsets(LocationGroup locationGroup, out TimeSpan start, out TimeSpan end)
        {
            start = end = TimeSpan.Zero;

            if (locationGroup.Locations.Length == 0)
                return;

            Location startLocation = locationGroup.Locations[0];
            Location endLocation = locationGroup.Locations[locationGroup.Locations.Length - 1];

            start = GetTimeTaken(this.Start, startLocation);
            end = start + GetTimeTaken(startLocation, endLocation);
        }
        
        public override string ToString()
        {
            return Name;
        }

        internal void FixupRoutePoints()
        {
            foreach (RoutePoint routePoint in deserializedRoutePoints)
            {
                routePoint.Location = idLocationMap[routePoint.LocationId];
                AddNextRoutePoint(routePoint);
            }

            deserializedRoutePoints = null;
        }

        internal LocationGroup TryGetApproximateLocationGroupBasedOnCellTowerId(string cellTowerId)
        {
            foreach (var point in locationRoutePointMap.Values)
            {
                if (IsApproximateMatch(cellTowerId, point.Location.CellTowerId))
                    return point.Location.LocationGroup;
            }

            return null;
        }

        private bool IsApproximateMatch(string currentCellTowerId, string originalCellTowerId)
        {
            string[] split1 = currentCellTowerId.Split('_');
            string[] split2 = originalCellTowerId.Split('_');

            if (split1.Length > 1 && split2.Length > 1)
                return split1[1] == split2[1];

            return false;
        }
    }
}
